{
  "nbformat": 4,
  "nbformat_minor": 0,
  "metadata": {
    "colab": {
      "provenance": []
    },
    "kernelspec": {
      "name": "python3",
      "display_name": "Python 3"
    },
    "language_info": {
      "name": "python"
    }
  },
  "cells": [
    {
      "cell_type": "markdown",
      "source": [
        "<font color=\"#a9a56c\" size=2> **@Author: Arif Kasim Rozani - (Team Operation Badar)** </font>\n"
      ],
      "metadata": {
        "id": "38_QpUXJOAII"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **1.  What is OOP?**\n",
        "<font color=gold> **Object-Oriented Programming (OOP)** is a programming paradigm that organizes software design around **objects and classes**, rather than functions and logic. An object is an instance of a class, which is a blueprint for creating objects. **OOP focuses on modeling real-world entities and their relationships in a more intuitive and structured way.** </font>\n",
        "\n",
        "For example, if you’re building a car simulation, you might create a Car class. Each car in your program would be an object (instance) of that class, with attributes like color, speed, and model, and behaviors like accelerate() or brake()."
      ],
      "metadata": {
        "id": "2QcA7llc6NEh"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Why use OOP?**\n",
        "\n",
        "1.  <font color=orange> **Modularity**:</font> OOP allows you to break down complex problems into smaller, reusable components (objects).\n",
        "\n",
        "2.  <font color=orange> **Reusability**:</font> Classes and objects can be reused across different parts of a program or in entirely different programs.\n",
        "\n",
        "3.  <font color=orange> **Maintainability**:</font> Code is easier to maintain and update because it’s organized into logical units.\n",
        "\n",
        "4.  <font color=orange> **Scalability**:</font> OOP makes it easier to scale and extend programs as requirements grow.\n",
        "\n",
        "5.  <font color=orange>**Real-world modeling**:</font> OOP closely mirrors real-world entities, making it easier to understand and design systems."
      ],
      "metadata": {
        "id": "0li7E1Ku6Sn2"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Key Principles of OOP**\n",
        "OOP is built on four fundamental principles:\n",
        "\n",
        "\n",
        "1.  Encapsulation\n",
        "2.  Abstraction\n",
        "3.  Inheritance\n",
        "4.  Polymorphism\n",
        "\n",
        "\n",
        "---\n",
        "\n"
      ],
      "metadata": {
        "id": "SM5C7Zlo6WgP"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "### 🔐 **Encapsulation**\n",
        "\n",
        "* Wraps data and methods into a single unit (class).  \n",
        "* Restricts direct access to internal data (using private/protected attributes).  \n",
        "* Helps maintain control, security, and integrity of the data.\n",
        "\n",
        "----------\n",
        "\n",
        "### 🎭 **Abstraction**\n",
        "\n",
        "* Hides complex implementation details from the user.  \n",
        "* Shows only the essential features and relevant information.  \n",
        "* Makes the interface simple while managing complexity behind the scenes.\n",
        "\n",
        "----------\n",
        "\n",
        "### 🧬 **Inheritance**\n",
        "\n",
        "* Allows a class (child) to inherit properties and behaviors from another (parent).  \n",
        "* Promotes code reusability and hierarchical classification.  \n",
        "* hild class can override or extend parent functionality.\n",
        "\n",
        "----------\n",
        "\n",
        "### 🔁 **Polymorphism**\n",
        "\n",
        "* Lets one interface be used for different underlying data types or classes.  \n",
        "* The same method name can behave differently based on the object.  \n",
        "* Supports flexibility and scalability in code design."
      ],
      "metadata": {
        "id": "r87X__HN6tUV"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **What is a Class?**\n",
        "\n",
        "<font color=gold>A class is a blueprint or template for creating objects. It defines the **attribute** (data) and **methods** (functions) that the objects created from the class will have. Think of a class as a cookie cutter, and the objects as the cookies made from it.</font>\n",
        "\n",
        "For example, if you’re creating a program to manage vehicles, you might define a Vehicle class with attributes like color, speed, and model, and methods like accelerate() and brake()."
      ],
      "metadata": {
        "id": "ygpOxEa-7Nvt"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **What is an Object?**\n",
        "\n",
        "An **object** is an instance of a class. It’s a specific realization of the class, with its own unique data. For example, if Vehicle is a class, then my_car and your_car could be objects (instances) of that class, each with its own color, speed, and model."
      ],
      "metadata": {
        "id": "gaH_qiay7R31"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Creating a Class in Python**\n",
        "\n",
        "To create a class in Python, you use the class keyword, followed by the class name (by convention, class names use PascalCase). Here’s the basic syntax:"
      ],
      "metadata": {
        "id": "2AczA7uH7XK1"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class ClassName:\n",
        "  pass\n",
        "    # Class body"
      ],
      "metadata": {
        "id": "iYWiXxPL9SCD"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Defining Attributes and Methods**\n",
        "\n",
        "\n",
        "- <font color=gold>**Attributes** are variables that belong to an object. They represent the object’s state or properties.\n",
        "- **Methods** are functions that belong to an object. They define the object’s behavior. </font>\n",
        "\n",
        "For example, in a Vehicle class, color and speed could be attributes, and accelerate() and brake() could be methods."
      ],
      "metadata": {
        "id": "kvmtwih29e17"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Instantiating Objects**\n",
        "\n",
        "To create an object (instance) of a class, you call the class name like a function. This invokes the **\\_\\_init\\_\\_** method (**the constructor**) to initialize the object.\n",
        "\n",
        "The process of creating object from class is called Instantiation\n",
        "\n",
        "__ (double underscore)"
      ],
      "metadata": {
        "id": "yzrIvabu9p8D"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **self**\n",
        "\n",
        "\n",
        "**<font color=gold>The self refers to the current instance of the class.** It’s used to access attributes and methods of the object within the class. It’s always the first parameter of any method in a class. But when you call the method, you don’t pass self manually — Python does that for you.✨</font>"
      ],
      "metadata": {
        "id": "oNu2dr6E9zBk"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Person:\n",
        "    def __init__(self, name):\n",
        "        self.name = name     # Public attribute, Instance Variable\n",
        "        self.state = \"Idle\"  # Default state\n",
        "\n",
        "    def running(self):\n",
        "        self.state = \"Running\"\n",
        "        print(f\"{self.name} is now running.\")\n",
        "\n",
        "    def walking(self):\n",
        "        self.state = \"Walking\"\n",
        "        print(f\"{self.name} is now walking.\")\n",
        "\n",
        "    def sleeping(self):\n",
        "        self.state = \"Sleeping\"\n",
        "        print(f\"{self.name} is now sleeping.\")\n",
        "\n",
        "    def show_state(self):\n",
        "        print(f\"{self.name} is currently in '{self.state}' state.\")"
      ],
      "metadata": {
        "id": "_aL3blSh-hcC"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "source": [
        "# Example usage:\n",
        "person1 = Person(\"Ali\")\n",
        "person1.show_state()"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "3rvH709w-kiq",
        "outputId": "a97a8179-dbd4-4878-8a21-3098449b8809"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Ali is currently in 'Idle' state.\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "person1.walking()\n",
        "person1.show_state()"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "rhKRqYG2-ooC",
        "outputId": "b11c5825-9bb5-402e-9a01-28d8fcb462da"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Ali is now walking.\n",
            "Ali is currently in 'Walking' state.\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "person1.sleeping()\n",
        "person1.show_state()"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "yo7_RCFC-ody",
        "outputId": "7c75d7d3-d754-4bc0-cb48-939b491f4984"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Ali is now sleeping.\n",
            "Ali is currently in 'Sleeping' state.\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **3.  Constructors and Destructors**\n",
        "\n",
        "In this section, we’ll explore **constructors** and **destructors** in Python. These are special methods that are automatically called when an object is created or destroyed, respectively. Understanding these concepts is essential for managing the lifecycle of objects in your programs."
      ],
      "metadata": {
        "id": "RRdFYNmJ-3-S"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **What is a Constructor?**\n",
        "\n",
        "A **constructor** is a special method that is automatically called when an object of a class is created. It’s used to initialize the object’s attributes or perform any setup required for the object.\n",
        "\n",
        "In Python, the constructor method is named \\_\\_init\\_\\_"
      ],
      "metadata": {
        "id": "bHR36BiU-8_S"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **The \\_\\_init__ Method**\n",
        "\n",
        "The \\_\\_init__ method is the most commonly used constructor in Python. It’s called whenever a new instance of a class is created. You can define this method to initialize the object’s attributes or perform other setup tasks.\n",
        "\n",
        "Syntax:"
      ],
      "metadata": {
        "id": "1sVLzxYa_IWS"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Parameterized Constructors**\n",
        "\n",
        "A **parameterized constructor** takes parameters (arguments) when creating an object. These parameters are used to initialize the object’s attributes.\n",
        "\n",
        "Example:"
      ],
      "metadata": {
        "id": "wDCx7y6__ZQC"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Person:\n",
        "    def __init__(self, name, age, address):\n",
        "        self.name = name\n",
        "        self.age = age\n",
        "        self.address = address\n",
        "\n",
        "# Create an object with a parameterized constructor\n",
        "person1 = Person(\"Alice\", 30, \"81 Harvard Avenue New York, NY 1012\")\n",
        "print(\"Name:    \", person1.name)  # Output: Alice\n",
        "print(\"Age:     \", person1.age)   # Output: 30\n",
        "print(\"Address: \",person1.address)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "B3mIcvqx_coq",
        "outputId": "fac7d8d0-5769-4281-f40b-8c249d6e515c"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Name:     Alice\n",
            "Age:      30\n",
            "Address:  81 Harvard Avenue New York, NY 1012\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **The \\_\\_del__ Method (Destructor)**\n",
        "\n",
        "A **destructor** is a special method that is called when an object is about to be destroyed. In Python, the destructor method is named \\_\\_del__. It’s used to perform cleanup tasks, such as releasing resources or closing files.\n",
        "\n",
        "Syntax:"
      ],
      "metadata": {
        "id": "SKS_hAzLAJTy"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Car:\n",
        "    # Parameterized constructor\n",
        "    def __init__(self, brand, model):\n",
        "        self.brand = brand\n",
        "        self.model = model\n",
        "\n",
        "    # Destructor\n",
        "    def __del__(self):\n",
        "        print(f\"The {self.brand} {self.model} has been destroyed.\")\n",
        "\n",
        "\n",
        "# Create an object of the Car class\n",
        "my_car = Car(\"Toyota\", \"Corolla\")\n",
        "# Explicitly delete the object (triggers the destructor)\n",
        "del my_car  # Output: The Toyota Corolla has been destroyed."
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "RYf5kklDAPsx",
        "outputId": "5ad00f4b-4bf9-4bd5-c2ca-628e12de3e78"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "The Toyota Corolla has been destroyed.\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Access Modifiers: Public, Private, and Protected**\n",
        "\n",
        "In Python, access control is implemented using naming conventions rather than strict access modifiers like in other languages (e.g., Java or C++). Here’s how it works:\n",
        "\n",
        "1.  **Public**:\n",
        "  * Attributes and methods are accessible from anywhere.\n",
        "  * No special syntax is used.\n",
        "Example: name\n",
        "\n",
        "2.  Protected:\n",
        "  - Attributes and methods are intended for internal use within the class and its **subclasses**.\n",
        "  - A single underscore _ is used as a prefix.\n",
        "Example: _age\n",
        "\n",
        "3.  Private:\n",
        "  - Attributes and methods are accessible only within the class itself.\n",
        "  - A double underscore __ is used as a prefix.\n",
        "Example: __salary"
      ],
      "metadata": {
        "id": "SWUJD06bBNM5"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Protected:**"
      ],
      "metadata": {
        "id": "EhfbTKErE1ud"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "# 🎩 The Wise Father Living in Karachi Pakistan\n",
        "class Father:\n",
        "    def __init__(self):\n",
        "        self._secret_location = \"Under the old oak tree\"  # Protected attribute\n",
        "\n",
        "    def _where_i_hide_the_gold(self):\n",
        "        return f\"The gold is hidden at: {self._secret_location}\""
      ],
      "metadata": {
        "id": "VxnlrZmxE4u9"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "source": [
        "# 🧒 The Sneaky but Trusted Son\n",
        "class Son(Father): #  Inheritance\n",
        "  pass"
      ],
      "metadata": {
        "id": "GX1o7tt-E-rd"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "source": [
        "son = Son()\n",
        "son._where_i_hide_the_gold() # Accessing protected method from parent"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 36
        },
        "id": "j8BZdB0zFD0V",
        "outputId": "ed41061d-3f7f-4e50-e229-5a58c43b7bec"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "'The gold is hidden at: Under the old oak tree'"
            ],
            "application/vnd.google.colaboratory.intrinsic+json": {
              "type": "string"
            }
          },
          "metadata": {},
          "execution_count": 11
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **❌ Not recommended, still accessible**"
      ],
      "metadata": {
        "id": "sbWW1qAxGOM8"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "# 🧑‍🦳 The Curious Relative Living in Paris France\n",
        "class Relatives:\n",
        "    def __init__(self):\n",
        "        self.father = Father()\n",
        "\n",
        "    def try_access_secret(self):\n",
        "            print(self.father._secret_location)  # ⚠️ Not recommended, still accessible\n",
        "            print(self.father._where_i_hide_the_gold())  # ⚠️ Technically works, but discouraged"
      ],
      "metadata": {
        "id": "VZQtPdyGF7Wk"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "source": [
        "print(\"\\n---- RELATIVE'S ACCESS ----\")\n",
        "relative = Relatives()\n",
        "relative.try_access_secret()"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "4n0wRCujGCoM",
        "outputId": "2ef51566-b6ba-4e39-b972-d28360611ef5"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "\n",
            "---- RELATIVE'S ACCESS ----\n",
            "Under the old oak tree\n",
            "The gold is hidden at: Under the old oak tree\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **Private:**"
      ],
      "metadata": {
        "id": "JBcZE4wVEtCW"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Employee:\n",
        "    def __init__(self, name, salary):\n",
        "        self.name = name\n",
        "        self.__salary = salary  # Private attribute\n",
        "\n",
        "    def show_salary(self):\n",
        "        print(f\"{self.name}'s salary is {self.__salary}\")\n",
        "\n",
        "emp = Employee(\"Ayesha\", 50000)\n",
        "emp.show_salary()\n",
        "\n",
        "\n",
        "# But it can still be accessed using name mangling (not recommended)\n",
        "print(emp._Employee__salary)  # ✅ Works, but use with caution"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "KloY7LtSBh-5",
        "outputId": "465bdf73-74b8-439f-ae08-f4b0279822af"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Ayesha's salary is 50000\n",
            "50000\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "# Trying to access __salary directly will cause an error\n",
        "# print(emp.__salary)  ❌ AttributeError\n",
        "print(emp.__salary)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 280
        },
        "id": "OthYjeN1BqAA",
        "outputId": "5ac6d461-99f1-48d7-dc29-10ea37b1574c"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "error",
          "ename": "AttributeError",
          "evalue": "'Employee' object has no attribute '__salary'",
          "traceback": [
            "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
            "\u001b[0;31mAttributeError\u001b[0m                            Traceback (most recent call last)",
            "\u001b[0;32m<ipython-input-72-35b676e6583c>\u001b[0m in \u001b[0;36m<cell line: 0>\u001b[0;34m()\u001b[0m\n\u001b[1;32m      1\u001b[0m \u001b[0;31m# Trying to access __salary directly will cause an error\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m      2\u001b[0m \u001b[0;31m# print(emp.__salary)  ❌ AttributeError\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m----> 3\u001b[0;31m \u001b[0mprint\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0memp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m__salary\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
            "\u001b[0;31mAttributeError\u001b[0m: 'Employee' object has no attribute '__salary'"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "## Double Underscore `__salary` – _Name Mangling for Privacy_\n",
        "\n",
        "### 📌 Meaning:\n",
        "\n",
        "-   When you prefix a method or variable name with **double underscores**, Python **\"name-mangles\"** it.\n",
        "    \n",
        "-   This makes the attribute harder to accidentally override or access from outside the class.\n",
        "    \n",
        "-   It’s used when you're trying to **avoid name conflicts** in subclasses.\n",
        "    \n",
        "-   Python renames `__salary` to `_ClassName__salary`."
      ],
      "metadata": {
        "id": "cYoIS6j4CHM3"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "print(emp._Employee__salary) # accessed using name mangling (not recommended)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "T6N2vLWvCQJi",
        "outputId": "b6016056-0075-496e-e901-dc55d5826ff2"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "50000\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Types of Inheritance**\n",
        "\n",
        "  1.  **Single Inheritance:**\n",
        "\n",
        "      A subclass inherits from a single superclass.\n",
        "\n",
        "  2.  **Multiple Inheritance:**\n",
        "\n",
        "      A subclass inherits from multiple superclasses."
      ],
      "metadata": {
        "id": "L_nVPlD3HeDL"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **The super() Function**\n",
        "The super() function is used to call methods from the parent class. It’s particularly useful in the constructor (__init__) to initialize attributes from the parent class."
      ],
      "metadata": {
        "id": "Eq_u5J0CHh3r"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Method Overriding**\n",
        "\n",
        "**Method overriding** occurs when a subclass provides a specific implementation of a method that is already defined in its superclass. The subclass method **overrides** the superclass method."
      ],
      "metadata": {
        "id": "aVsBKtbhHkVz"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "# Parent class\n",
        "class Animal:\n",
        "    def __init__(self, name):\n",
        "        self.name = name\n",
        "\n",
        "    def speak(self):\n",
        "        return \"Animal sound\"\n",
        "\n",
        "# Child class (inherits from Animal)\n",
        "class Dog(Animal):\n",
        "\n",
        "    def __init__(self, name):\n",
        "      super().__init__(name) # Call the parent class constructor\n",
        "\n",
        "    def speak(self): # Overriding the parent method speak()\n",
        "        return \"Woof!\"\n",
        "\n",
        "# Create an instance of the Dog class\n",
        "dog = Dog(\"Tommy\")\n",
        "print(f\"{dog.name} says {dog.speak()}\")  # Output: Buddy says Woof!"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "jfAV0FLuHuns",
        "outputId": "560b3ee2-e8b9-4372-9249-d291c5b97007"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Tommy says Woof!\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Polymorphism Example**"
      ],
      "metadata": {
        "id": "FGszZ64RJyI6"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "animal: Animal = Dog(\"Tommy\")\n",
        "print(f\"{animal.name} says {animal.speak()}\")"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "C0YVgMbYJ8uB",
        "outputId": "0761ec61-714f-4cdd-8d3d-cf270abb120d"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Tommy says Woof!\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Multiple Inheritance**"
      ],
      "metadata": {
        "id": "kRmxV6hmIhg0"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "# First parent class\n",
        "class Bird:\n",
        "    def fly(self):\n",
        "        return \"Flying high!\"\n",
        "\n",
        "# Second parent class\n",
        "class Fish:\n",
        "    def swim(self):\n",
        "        return \"Swimming deep!\"\n",
        "\n",
        "# Child class (inherits from both Bird and Fish)\n",
        "class FlyingFish(Bird, Fish):\n",
        "    pass\n",
        "\n",
        "# Create an instance of the FlyingFish class\n",
        "flying_fish = FlyingFish()\n",
        "print(flying_fish.fly())  # Output: Flying high!\n",
        "print(flying_fish.swim())  # Output: Swimming deep!"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "Jf-WpBUtIlMj",
        "outputId": "8739d43e-08a9-4cd0-e544-4a216c753664"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Flying high!\n",
            "Swimming deep!\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Operator Overloading:**\n",
        "\n",
        "  Customizing the behavior of operators (e.g., +, -, *, etc.) for user-defined classes using special methods like __add__, __sub__, etc."
      ],
      "metadata": {
        "id": "6G217lhpKa25"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Point:\n",
        "    def __init__(self, x, y):\n",
        "        self.x = x    # Instance Variable\n",
        "        self.y = y    # instance Variable\n",
        "\n",
        "    # Overload the + operator\n",
        "    def __add__(self, other):\n",
        "        return Point(self.x + other.x, self.y + other.y)\n",
        "\n",
        "    # Overload the str() function for printing\n",
        "    def __str__(self):\n",
        "        return f\"Point({self.x}, {self.y})\"\n",
        "\n",
        "# Create instances of Point\n",
        "point_1 = Point(1, 2)\n",
        "point_2 = Point(3, 4)"
      ],
      "metadata": {
        "id": "HiayaN8fKlWB"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "source": [
        "# Use the + operator to add two points\n",
        "point_3 = point_1 + point_2\n",
        "print(point_3)  # Output: Point(4, 6)"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "SMsC3LCdKt3A",
        "outputId": "ab06070e-af9c-4422-edff-6985a6a1f678"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Point(4, 6)\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Duck Typing**\n",
        "\n",
        "Duck typing is a fundamental concept in Python programming that enables developers to write flexible and dynamic code. It's a key aspect of Python's philosophy, which emphasizes \"we are all consenting adults here\" and encourages a more relaxed approach to programming.\n",
        "\n"
      ],
      "metadata": {
        "id": "1NTowEjMLaeY"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Duck:\n",
        "    def speak(self):\n",
        "        return self.__repr__() + \"   : Quack!\"\n",
        "\n",
        "class Person:\n",
        "    def speak(self):\n",
        "        return self.__repr__() + \" : I can also quack like a duck!\"\n",
        "\n",
        "# Function to demonstrate duck typing\n",
        "def make_it_quack(duck):\n",
        "    print(duck.speak()) # type(thing),\": \", >>> instead of using type() method we used self.__repr__() to print the type of the object\n",
        "\n",
        "# Create instances of Duck and Person\n",
        "duck = Duck()\n",
        "person = Person()\n",
        "\n",
        "# Call the function with different objects\n",
        "make_it_quack(duck)    # Output: Quack!\n",
        "make_it_quack(person)  # Output: I can quack like a duck!"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "DZ0_piesLdIw",
        "outputId": "cc3c7fc1-e4f3-454a-9fdc-765e11b45998"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "<__main__.Duck object at 0x7c52a19af910>   : Quack!\n",
            "<__main__.Person object at 0x7c52a19bf350> : I can also quack like a duck!\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **What is Abstraction?**\n",
        "\n",
        "Abstraction simplifies complex systems by breaking them into manageable parts. For example, all vehicles (cars, bikes) must have a start() method, but each vehicle starts differently. Abstraction lets us enforce this rule without worrying about implementation details.\n",
        "\n",
        "## **Abstract Classes and Methods**\n",
        "\n",
        "These are \"incomplete\" templates you can't use directly. They:\n",
        "  * Define required methods for child classes\n",
        "  * Ensure consistency across related classes\n",
        "  * Prevent creating objects from the abstract class itself\n",
        "\n",
        "<br>\n",
        "\n",
        "Using Python's abc Module\n",
        "\n",
        "We create abstract classes using:"
      ],
      "metadata": {
        "id": "mbVgEb_0NAh2"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "from abc import ABC, abstractmethod\n",
        "\n",
        "class Vehicle(ABC):\n",
        "    @abstractmethod\n",
        "    def start(self):\n",
        "        pass"
      ],
      "metadata": {
        "id": "xj3DiX5uNEGv"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Key components:**\n",
        "\n",
        "1.  **ABC** makes the class abstract\n",
        "\n",
        "2.  **@abstractmethod** marks required methods\n",
        "\n",
        "3.  Child classes **must** implement all **abstract** **methods**"
      ],
      "metadata": {
        "id": "xx8CrztJNIuf"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Truck(Vehicle): # Child class must implement abstract method\n",
        "    pass"
      ],
      "metadata": {
        "id": "3mhwDWcpNO_m"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **<font color=red>Must Implement Abstract Method:</font>**"
      ],
      "metadata": {
        "id": "cEwgqmU0ir2-"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "truck: Truck = Truck() # ❌ TypeError: Can't instantiate abstract class Truck with abstract method start"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 141
        },
        "id": "7Z0OUclYNax2",
        "outputId": "9e09715b-d2e9-4401-d167-05c88e9fb93b"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "error",
          "ename": "TypeError",
          "evalue": "Can't instantiate abstract class Truck with abstract method start",
          "traceback": [
            "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
            "\u001b[0;31mTypeError\u001b[0m                                 Traceback (most recent call last)",
            "\u001b[0;32m<ipython-input-66-11084430decf>\u001b[0m in \u001b[0;36m<cell line: 0>\u001b[0;34m()\u001b[0m\n\u001b[0;32m----> 1\u001b[0;31m \u001b[0mtruck\u001b[0m\u001b[0;34m:\u001b[0m \u001b[0mTruck\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mTruck\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m",
            "\u001b[0;31mTypeError\u001b[0m: Can't instantiate abstract class Truck with abstract method start"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "class Truck(Vehicle):\n",
        "    def start(self):\n",
        "        return \"Truck started\""
      ],
      "metadata": {
        "id": "rK0OPSG9NpkW"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "source": [
        "truck: Truck = Truck()\n",
        "print(truck.start())"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "191vHA8INvBu",
        "outputId": "d62896fb-bba1-44ad-8fd6-320d01fddda8"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Truck started\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [],
      "metadata": {
        "id": "ZPuQx7sT5Lwc"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **🔁 Class variable (shared by all instances)**"
      ],
      "metadata": {
        "id": "3Z58_RgMuN7Q"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class City:\n",
        "    name = \"Unknown City\"  # 🔁 Class variable (shared by all instances)\n",
        "\n",
        "    def __init__(self, name):\n",
        "       City.name = name\n",
        "\n",
        "    def show_info(self):\n",
        "        print(f\"City Name: {City.name} \")\n",
        "\n",
        "\n",
        "# 🌆 Creating city instances\n",
        "city1 = City(\"Kolachi\")\n",
        "city2 = City(\"Kolachi\")\n",
        "\n",
        "# 🖨 Showing info\n",
        "city1.show_info()  # City Name: Kolachi\n",
        "city2.show_info()  # City Name: Kolachi"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "1HB0oCtU5MHc",
        "outputId": "317a3b3d-1a08-42c2-b694-5a4ad25571ff"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "City Name: Kolachi \n",
            "City Name: Kolachi \n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "# 🧠 Changing class variable affects all instances\n",
        "City.name = \"Karachi\""
      ],
      "metadata": {
        "id": "Wr6MEC_wAin-"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "source": [
        "city1.show_info()  # City Name: Karachi\n",
        "city2.show_info()  # City Name: Karachi"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "_Pxev0wb5-sl",
        "outputId": "a00ec200-f2b8-4ae6-cd22-8d75f62859a9"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "City Name: Karachi \n",
            "City Name: Karachi \n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "# **5.  Methods in Python Classes**\n",
        "\n",
        "In Python, methods are functions defined within a class that operate on the class’s data. There are three types of methods in Python: **instance methods**, **class methods**, and **static methods**. Each type has a specific purpose and use case. Let’s explore them in detail."
      ],
      "metadata": {
        "id": "Nkl3TO5S7P5q"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **1. Instance Methods**\n",
        "\n",
        "- **Definition**: Instance methods are the most common type of methods. They operate on an instance of the class and can access and modify instance attributes.\n",
        "\n",
        "- **First Parameter**: <font color=gold>The first parameter is always self, which refers to the instance of the class.</font>\n",
        "\n",
        "- **Usage**: Used for methods that need to access or modify instance-specific data."
      ],
      "metadata": {
        "id": "GCt20Cvs7Vbq"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **2. Class Methods (@classmethod)**\n",
        "\n",
        "- **Definition**: Class methods operate on the class itself rather than on instances. They can access and modify class attributes.\n",
        "\n",
        "- **First Parameter**: <font color=gold>The first parameter is always cls, which refers to the class.</font>\n",
        "- **Decorator**: Defined using the @classmethod decorator.\n",
        "\n",
        "- **Usage**: Used for methods that need to work with class-level data or perform operations related to the class."
      ],
      "metadata": {
        "id": "9iKjm6IW7YYi"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **3. Static Methods (@staticmethod)**\n",
        "\n",
        "- **Definition**: <font color=gold>Static methods are independent of both the class and its instances. They don’t have access to self or cls.</font>\n",
        "\n",
        "- **Decorator**: Defined using the @staticmethod decorator.\n",
        "\n",
        "- **Usage**: Used for utility functions that don’t depend on class or instance data."
      ],
      "metadata": {
        "id": "IQzcFrc67c6y"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "| Aspect               | Instance Methods                          | Class Methods                          | Static Methods                             |\n",
        "|----------------------|-------------------------------------------|----------------------------------------|--------------------------------------------|\n",
        "| First Parameter      | self (refers to the instance)             | cls (refers to the class)              | No specific parameter                      |\n",
        "| Access to Attributes | Can access and modify instance attributes | Can access and modify class attributes | Cannot access instance or class attributes |\n",
        "| Decorator            | None                                      | @classmethod                           | @staticmethod                              |\n",
        "| Usage                | For methods that work with instance data  | For methods that work with class data  | For utility functions                      |\n",
        "|                      |                                           |                                        "
      ],
      "metadata": {
        "id": "eIzDJdhT7lAx"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Class Methods (@classmethod)**"
      ],
      "metadata": {
        "id": "lbF5-vyl8tGS"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Human:\n",
        "\n",
        "  specie = \"Homo sapiens\" # Class attribute\n",
        "\n",
        "  @classmethod\n",
        "  def get_specie(cls): # python environment will automatically pass the 'cls' as a parameter\n",
        "    return cls.specie"
      ],
      "metadata": {
        "id": "PrZQpGv572t7"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "source": [
        "Human.get_specie()"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/",
          "height": 36
        },
        "id": "t2Ft2cOL8b_i",
        "outputId": "b8e559e2-48e1-4717-eb49-c3e50db7800f"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "execute_result",
          "data": {
            "text/plain": [
              "'Homo sapiens'"
            ],
            "application/vnd.google.colaboratory.intrinsic+json": {
              "type": "string"
            }
          },
          "metadata": {},
          "execution_count": 54
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **Static Methods (@staticmethod)**"
      ],
      "metadata": {
        "id": "JgqYeGaR9WIw"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Human:\n",
        "    @staticmethod\n",
        "    def breathe():\n",
        "        print(\"Breathing...\")"
      ],
      "metadata": {
        "id": "uMWtXIza9YNq"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "source": [
        "Human.breathe()"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "tp-tX_zv-mMo",
        "outputId": "35e5f507-2723-4b92-bfb8-2199893ad95d"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "Breathing...\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "## **<font color=orange>If I can access class stuff using the class name, why bother with cls?</font>**"
      ],
      "metadata": {
        "id": "274qq9Oglh6F"
      }
    },
    {
      "cell_type": "code",
      "source": [
        "class Animal:\n",
        "    species = \"Unknown\"\n",
        "\n",
        "    @classmethod\n",
        "    def make(cls):\n",
        "        return cls()  # ← this is the magic\n",
        "\n",
        "class Dog(Animal):\n",
        "    species = \"Dog\""
      ],
      "metadata": {
        "id": "DN5kKy7Flr_N"
      },
      "execution_count": null,
      "outputs": []
    },
    {
      "cell_type": "code",
      "source": [
        "animal = Animal.make()\n",
        "print(type(animal))"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "cqMMmPHrlzFM",
        "outputId": "a6ddedcc-4241-419d-a0c5-6248a7a65a02"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "<class '__main__.Animal'>\n"
          ]
        }
      ]
    },
    {
      "cell_type": "code",
      "source": [
        "dog = Dog.make()\n",
        "print(type(dog))  # <class '__main__.Dog'>"
      ],
      "metadata": {
        "colab": {
          "base_uri": "https://localhost:8080/"
        },
        "id": "_KKJn0Ojl41m",
        "outputId": "4c9241b5-f121-4871-f210-81de000eeee6"
      },
      "execution_count": null,
      "outputs": [
        {
          "output_type": "stream",
          "name": "stdout",
          "text": [
            "<class '__main__.Dog'>\n"
          ]
        }
      ]
    },
    {
      "cell_type": "markdown",
      "source": [
        "✅ Because we used cls(), it returned a Dog instance, even though the method was defined in the Animal base class."
      ],
      "metadata": {
        "id": "zwcOBtjEmpEN"
      }
    },
    {
      "cell_type": "markdown",
      "source": [
        "\n",
        "\n",
        "| Access Pattern | Flexibility | Best Used In                                                             |   |\n",
        "|----------------|-------------|--------------------------------------------------------------------------|---|\n",
        "| cls            | Dynamic     | Classmethods that support inheritance, polymorphism, and factory methods |   |\n",
        "| Class name     | Static      | When you don’t care about inheritance or subclass behavior               |   |\n",
        "\n"
      ],
      "metadata": {
        "id": "OtF9WG1XmYhU"
      }
    }
  ]
}